/*
 * Copyright (c) Huawei Technologies Co., Ltd. 2021-2022. All rights reserved.
 *
 */

import * as fs from 'fs';
import os from 'os';
import path from 'path';

import { DefaultTask, hvigorCore, HvigorCoreNode, MetricFactory, MetricLogType, TcbStore } from '@ohos/hvigor';
import { isCI } from '@ohos/hvigor-common';
import fse from 'fs-extra';

import { BuildDirConst } from '../../const/build-directory-const.js';
import { AppPlugin } from '../../plugin/app-plugin.js';
import { OhosPluginId } from '../../plugin/common/ohos-plugin-id.js';
import { SdkInfo } from '../../sdk/sdk-info.js';
import { InjectUtil } from '../../utils/inject-util.js';
import { OhosLogger } from '../../utils/log/ohos-logger.js';
import { ProcessUtils } from '../../utils/process-utils.js';
import { getBuildModeName } from '../../utils/task-util.js';
import { ValidatorStore } from '../../utils/validate/validator-store.js';
import { GlobalProjectDataService } from '../service/global-project-data-service.js';
import { ModuleTaskService } from '../service/module-task-service.js';
import { TaskService } from '../service/task-service.js';
import { TaskNames } from './task-names.js';

/**
 * module级别的clean
 *
 * @since 2022/1/10
 */
export class Clean extends DefaultTask {
  private readonly _taskService: TaskService;

  private _logger = OhosLogger.getLogger(Clean.name);

  constructor(node: HvigorCoreNode, taskService: TaskService) {
    super(node, TaskNames.CommonTask.CLEAN);
    this._taskService = taskService;
  }

  // 声明模块级native clean任务
  nativeClean = (sdkInfo: SdkInfo | undefined) => {
    if (!sdkInfo) {
      return;
    }
    const processUtils = new ProcessUtils();
    if (!(this._taskService instanceof ModuleTaskService)) {
      return;
    }
    const archDir = path.resolve(this._taskService.getModuleModel().getProjectDir(), '.cxx');
    const ninjaBin = sdkInfo.getNativeNinjaTool();
    if (!fs.existsSync(archDir) || !fs.existsSync(ninjaBin)) {
      return;
    }
    const archDirectories: string[] = [];
    this.getArchDirectories(archDir, archDirectories, this._taskService);
    const warns: string[] = [];
    for (const archDir of archDirectories) {
      const cachePath = path.resolve(archDir, 'CMakeCache.txt');
      const buildNinjaPath = path.resolve(archDir, 'build.ninja');
      const isExistRulesNinja = fs.existsSync(path.resolve(archDir, 'rules.ninja')) || fs.existsSync(path.resolve(archDir, 'CMakeFiles', 'rules.ninja'));
      if (!fs.existsSync(cachePath) || !fs.existsSync(buildNinjaPath) || !isExistRulesNinja) {
        continue;
      }

      // 执行ninja clean
      const commands: string[] = [ninjaBin, '-C', archDir, 'clean'];
      this.tryCatchExecute(commands, warns, archDir, processUtils);
      if (fs.existsSync(cachePath)) {
        fs.unlinkSync(cachePath);
      }
    }
    if (warns.length > 0) {
      let warnMessage = 'Delete the following directories that are no longer needed:';
      for (const warnPath of warns) {
        warnMessage = warnMessage.concat(os.EOL, warnPath);
      }
      this._logger.warn(warnMessage);
    }
  };

  tryCatchExecute = (commands: string[], warns: string[], archDir: string, processUtils: ProcessUtils): void => {
    try {
      processUtils.executeSync(commands);
    } catch (e) {
      warns.push(archDir);
    }
  };

  getArchDirectories = (directoryPath: string, archDirectories: string[], taskService: ModuleTaskService) => {
    const buildMode = getBuildModeName();
    for (const productName of taskService.getProjectModel().getProductNames()) {
      for (const target of taskService.getTargets()) {
        const archPath = path.resolve(directoryPath, productName, target.getTargetName(), buildMode);
        this.pushArchDirectories(archPath, archDirectories);
      }
    }
  };

  private pushArchDirectories(archPath: string, archDirectories: string[]) {
    if (!fs.existsSync(archPath) || !this.isDirectory(archPath)) {
      return;
    }
    const files = fs.readdirSync(archPath);
    for (const file of files) {
      const filePath = path.join(archPath, file);
      if (file === 'arm64-v8a' || file === 'armeabi-v7a' || file === 'x86_64') {
        if (!this.isDirectory(filePath)) {
          continue;
        }
        archDirectories.push(filePath);
      }
    }
  }

  private isDirectory(directoryPath: string): boolean {
    try {
      fs.readdirSync(directoryPath);
      return true;
    } catch (err) {
      return false;
    }
  }

  private resetRemoteHspCache() {
    /**
     * GlobalProjectDataService初始化先于clean；
     * 如果是rebuild，执行到这里的时候，remote hsp的签名缓存已经在GlobalProjectDataService读取了
     * 但这里又清掉build的话，后面的任务如sign-module-remote-hsp就会出现判断错误
     * 所以，这里也需要把GlobalProjectDataService的remote hsp签名缓存清掉
     */
    GlobalProjectDataService.getInstance().resetRemoteHspCache();
    GlobalProjectDataService.getInstance().getIntegratedHspUtils()?.resetIntegratedHspCache();
  }

  registryAction = (): Function => {
    return async () => {
      await this.terminateWorkerPool();

      // 即使taskService是模块级别，也需要清理工程级的build目录，其中可能会包含签名的remote hsp等模块级的构建产物
      await this.clean(path.resolve(this._taskService.getProjectModel().getProjectDir(), 'build'));
      this.resetRemoteHspCache();
      if (this._taskService instanceof ModuleTaskService) {
        const moduleModel = this._taskService.getModuleModel();
        if (!moduleModel) {
          return;
        }
        await this.clean(path.resolve(moduleModel.getProjectDir(), 'build'), path.resolve(moduleModel.getProjectDir(), '.test'));
      }
    };
  };

  clean = async (buildDir: string, testBuildDir?: string) => {
    let hasError = false;
    hasError ||= this.rmdirSyncWithBuildDir(buildDir);
    // 删除测试框架构建参数.test目录
    if (testBuildDir) {
      hasError ||= this.rmdirSyncWithBuildDir(testBuildDir);
    }

    // Safety: hvigor.getProject()必然已经被初始化
    const customBuildDir = InjectUtil.getBuildCacheParentDir(
      buildDir,
      path.join(hvigorCore.getProject()!.getName(), buildDir.substring(hvigorCore.getProject()!.getNodeDir().length)),
    );
    if (customBuildDir !== buildDir) {
      hasError ||= this.rmdirSyncWithBuildDir(customBuildDir);

      // 如果是CI场景，清除掉.cxx文件夹
      if (isCI()) {
        const cxxDir = path.join(path.dirname(customBuildDir), BuildDirConst.CPP_OUTPUT);
        hasError ||= this.rmdirSyncWithBuildDir(cxxDir);
        if (!hasError) {
          this._logger.debug('CI scenario cleared .cxx dir.');
        }
      }
    }
    if (hasError) {
      this._logger.printErrorExit('UNABLE_TO_REMOVE_BUILD_DIR', [buildDir]);
    }

    if (this.node.classKind === 'project') {
      return;
    }
    const sdkInfo = (this._taskService.getProjectModel().getProject().getPluginById(OhosPluginId.OHOS_APP_PLUGIN) as AppPlugin).getTaskService()?.getSdkInfo();
    this.nativeClean(sdkInfo);
  };

  terminateWorkerPool = async () => {
    if (this.getWorkerPool().isActive()) {
      this._logger.debug('clean: Start terminating worker pool.');
      await this.getWorkerPool().terminate();
      this._logger.debug('clean: Terminating worker pool finished.');
      TcbStore.clear();
      ValidatorStore.clear();
    } else {
      this._logger.debug('clean: Worker pool is inactive.');
    }
  };

  rmdirSyncWithBuildDir = (buildDir: string) => {
    let hasError = false;
    if (fs.existsSync(buildDir)) {
      hasError = this.rmdirSync(buildDir, false);
    }

    if (hasError) {
      this._logger.printErrorExit('UNABLE_TO_REMOVE_BUILD_DIR', [buildDir]);
    }
    return hasError;
  };

  rmdirSync = (dirPath: string, hasError: boolean) => {
    fs.readdirSync(dirPath).forEach((name) => {
      const filePath = path.resolve(dirPath, name);
      const fileStats = fs.lstatSync(filePath);
      if (fileStats.isFile() || fileStats.isSymbolicLink()) {
        try {
          fs.unlinkSync(filePath);
        } catch (e: any) {
          MetricFactory.createLogEvent(e.message, MetricLogType.DEBUG);
          MetricFactory.createLogEvent(`Cannot delete ${filePath}. The file may be opened by another program.`, MetricLogType.WARN);
          this._logger.debug(e.message);
          this._logger.warn(`Cannot delete ${filePath}. The file may be opened by another program.`);
          hasError = true;
        }
      } else if (fileStats.isDirectory()) {
        hasError = this.rmdirSync(filePath, hasError);
      }
    });
    try {
      let canRm = true;
      if (!fs.existsSync(dirPath)) {
        return hasError;
      }
      const files = fs.readdirSync(dirPath);
      files.forEach((name) => {
        const filePath = path.resolve(dirPath, name);
        if (fs.existsSync(filePath)) {
          canRm = false;
        }
      });
      canRm && fse.removeSync(dirPath);
    } catch (e: any) {
      MetricFactory.createLogEvent(e.message, MetricLogType.DEBUG);
      MetricFactory.createLogEvent(`Unable to delete ${dirPath}. A file may be open by another program.`, MetricLogType.WARN);
      this._logger.debug(e.message);
      this._logger.warn(`Unable to delete ${dirPath}. A file may be open by another program.`);
      hasError = true;
    }
    return hasError;
  };
}
